/******************************************************************
*
*	CyberHTTP for Java
*
*	Copyright (C) Satoshi Konno 2002
*
*	File: HTTPPacket.java
*
*	Revision;
*
*	11/18/02
*		- first revision.
*	09/02/03
*		- Giordano Sassaroli <sassarol@cefriel.it>
*		- Problem : The API is unable to receive responses from the Microsoft UPnP stack

*
*******************************************************************/

package com.cidero.http;

import java.io.*;
import java.util.*;
import java.util.logging.Logger;
import java.util.Calendar;

import org.cybergarage.util.*;
import com.cidero.util.BufferedReaderInputStream;

/**
 * Abstract base class for HTTPRequest,HTTPResponse
 */
public abstract class HTTPPacket 
{
  private static Logger logger = Logger.getLogger("com.cidero.http");

  // Constants used in the reading of packet content when there is
  // no CONTENT-LENGTH header specified
  private static int MAX_UNSPECIFIED_CONTENT_SIZE = 4000000;
  private static int CONTENT_FRAGMENT_SIZE = 65536;

	private String firstLine = "";
	private Vector httpHeaderList = new Vector();
	private byte[] content = null;
  private byte[] hdrTmp = new byte[8192];

	////////////////////////////////////////////////
	//	Constructor
	////////////////////////////////////////////////
	
	public HTTPPacket()
	{
	}

  /**
   * Read header lines and store them in array. Blank line signifies 
   * end of HTTP header
   *
   * Notes:  Don't want to use InputReader to do the actual input since
   * that would conflict with the BufferedInputStream used for inputing
   * the content data (raw byte vs char data).  So custom version of
   * BufferedInputStream that includes support for readLine() is used
   */
  public void readHeaderLines( BufferedReaderInputStream inputStream ) 
    throws IOException
  {
    String headerLine = inputStream.readLine();
    while ((headerLine != null) && (0 < headerLine.length()) )
    {
      HTTPHeader header = new HTTPHeader(headerLine);
      if (header.hasName() == true)
        setHeader(header);
      headerLine = inputStream.readLine();
    }
  }
  
  /**
   * Read packet content into byte array
   *
   * Some HTTP servers don't include a Content-Length header, but 
   * still include content. These servers just depend on the EOF 
   * of the socket connection to signal end-of-content.  To handle 
   * this, if the packet has no Content-Length header, the socket
   * is read until an EOF is encountered
   *
   */
  public void readContent( BufferedReaderInputStream inputStream )
    throws IOException
  {
    byte[] content;

    if (hasHeader(HTTP.CONTENT_LENGTH))
    {
      int contentLength = getContentLength();
      logger.fine("reading content with expected length " + contentLength );

      content= new byte[contentLength];

      if (contentLength > 0)
      {
        int readCnt = 0;
        while (readCnt < contentLength)
        {
          int len = inputStream.read( content, readCnt, 
                                      contentLength - readCnt );
          if (len < 0)
            break;
          readCnt += len;
        }

        if( readCnt != contentLength )
          throw new IOException("Short HTTP packet received - contentLength: " + contentLength + " received: " + readCnt );        

      }
    }
    else
    {
      logger.fine("reading content of unspecified length " );

      //
      // Read input stream in 64K fragments until EOF encountered, or 
      // max unspecified content length (nominally 4 MB) reached.
      // Fragments are reassembled as a final step
      //
      ArrayList contentFragmentList = new ArrayList();
      int contentLength = 0;
      byte[] fragment;

      try 
      {
        while( contentLength < MAX_UNSPECIFIED_CONTENT_SIZE )
        {
          int readLength = 0;
          fragment = new byte[CONTENT_FRAGMENT_SIZE];

          int fragmentReadCount = 0;
          while (fragmentReadCount < CONTENT_FRAGMENT_SIZE)
          {
            readLength = inputStream.read(fragment, fragmentReadCount,
                                   CONTENT_FRAGMENT_SIZE - fragmentReadCount);
            logger.fine("read len = " + readLength );

            if (readLength < 0)
              break;
            fragmentReadCount += readLength;
          }
          contentLength += fragmentReadCount;

          if( fragmentReadCount > 0 )
            contentFragmentList.add( fragment );

          if( readLength < 0 )
            break;
        }
      }
      catch( IOException e )
      {
        logger.fine("IOException reading content of unspecified length:" + e);
      }
      finally
      {
        // Copy 64K content fragments to single byte array
        content= new byte[contentLength];
        int contentCopyBytesRemaining = contentLength;
        for (int n = 0 ; n < contentFragmentList.size() ; n++)
        {
          int fragmentBytes = CONTENT_FRAGMENT_SIZE;
          if (fragmentBytes > contentCopyBytesRemaining)
            fragmentBytes = contentCopyBytesRemaining;

          System.arraycopy( (byte[])contentFragmentList.get(n), 0,
                            content, (contentLength-contentCopyBytesRemaining),
                            fragmentBytes );
          contentCopyBytesRemaining -= fragmentBytes;
        }
      }
    }

    setPacketContent( content );

  }
  
  /**
   * Write packet to output stream.  The header is written, followed by
   * zero or more content bytes. 
   */
  public void write( BufferedOutputStream outputStream ) throws IOException
  {
    // Get entire multi-line header as string, convert it to byte array
    // Returned header string includes trailing <CR><LF>
    String hdr = getHeaderString();
    byte[] hdrData = hdr.getBytes();
    outputStream.write( hdrData, 0, hdrData.length );

    logger.fine("Sent header: [" + hdr + "]" );
    
    int contentLength = getContentLength();
    logger.fine("content length = " + contentLength );

    if( contentLength > 0 )
    {
      if( contentLength != content.length )
        throw new IOException("Content buffer size mismatch");
      
      outputStream.write( content, 0, contentLength );
    }

    outputStream.flush();
  }

  public void writeHeader( BufferedOutputStream outputStream )
    throws IOException
  {
    // Get entire multi-line header as string, convert it to byte array
    // Returned header string includes trailing <CR><LF>
    String hdr = getHeaderString();
    byte[] hdrData = hdr.getBytes();
    outputStream.write( hdrData, 0, hdrData.length );

    outputStream.flush();
  }


  /** 
   * Set first line of HTTP packet. The first line will contain
   * the request (HEAD/GET/Etc..) or the response code line (200 OK)
   *
   * @param value   First line string
   */
	public void setFirstLine(String value)
	{
    firstLine = value;
	}
	
  /** 
   * Get first line of HTTP packet.
   *
   * @return First line string
   */
	public String getFirstLine()
	{
		return firstLine;
	}

	public abstract String getHeaderString();
  

	/**
   *	Get a single string containing all the header lines (name,value pairs)
   *  in the packet.
   *
   *  Note: Does *not* include blank line at the end (needs to be added at
   *  higher level) 
   */
	public String getAllHeaderLinesAsString()
	{
		StringBuffer str = new StringBuffer();
	
		int nHeaders = getNumHeaders();
		for (int n=0; n<nHeaders; n++) {
			HTTPHeader header = getHeader(n);
			str.append(header.getName() + ": " + header.getValue() + HTTP.CRLF);
		}
		
		return str.toString();
	}


  /** 
   * Get number of headers in HTTP packet
   *
   * @return  Number of headers
   */
	public int getNumHeaders()
	{
		return httpHeaderList.size();
	}

  /** 
   * Add a header to the HTTP packet
   *
   * @param header   Header object ( name, value pair )
   */
	public void addHeader(HTTPHeader header)
	{
		httpHeaderList.add(header);
	}

  /** 
   * Add a header to the HTTP packet
   *
   * @param name   Header field name
   * @param value  Header field value
   */
	public void addHeader(String name, String value)
	{
		HTTPHeader header = new HTTPHeader(name, value);
		httpHeaderList.add(header);
	}

	public boolean removeHeader(String name)
	{
		for (int n=0; n< httpHeaderList.size(); n++)
    {
			HTTPHeader header = (HTTPHeader)httpHeaderList.get(n);
			String headerName = header.getName();
			if (headerName.equalsIgnoreCase(name) == true)
      {
        httpHeaderList.remove(n);        
				return true;
      }
		}
		return false;
  }
  
  /**
   *  Get the header with the specified index from the list of headers
   *  for this packet
   *
   *  @param   index    Index of header in header list
   *
   *  @return  HTTP header (name,value pair)
   */
	public HTTPHeader getHeader(int index)
	{
		return (HTTPHeader)httpHeaderList.get(index);
	}
	

  /**
   *  Get the header with the specified name from the list of headers
   *  for this packet
   *
   *  @return  HTTP header (name,value pair), or null if no such header
   *           exists
   */
	public HTTPHeader getHeader(String name)
	{
		int nHeaders = getNumHeaders();
		for (int n=0; n<nHeaders; n++)
    {
			HTTPHeader header = getHeader(n);
			String headerName = header.getName();
			if (headerName.equalsIgnoreCase(name) == true)
				return header;			
		}
		return null;
	}

  /**
   *  Clear list of headers (so HTTPPacket object can be re-used)
   */
	public void clearHeaders()
	{
		httpHeaderList.clear();
		httpHeaderList = new Vector();
	}
	
  /**
   *  Test if header with given name exists in packet's header list
   *
   *  @param   name @return  true if header exists, otherwise false.
   *  @return  true if header exists, otherwise false.
   */
	public boolean hasHeader(String name)
	{
		return (getHeader(name) != null) ? true : false;
	}

	public void setHeader(String name, String value)
	{
		HTTPHeader header = getHeader(name);
		if (header != null) {
			header.setValue(value);
			return;
		}
		addHeader(name, value);
	}

	public void setHeader(String name, int value)
	{
		setHeader(name, Integer.toString(value));
	}

	public void setHeader(String name, long value)
	{
		setHeader(name, Long.toString(value));
	}
	
	public void setHeader(HTTPHeader header)
	{
		setHeader(header.getName(), header.getValue());
	}

  /**
   * Get value for a given header name 
   *
   * @return  value associated with given header name, or null if
   *          no such header exists in packet
   */
	public String getHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return null;
		return header.getValue();
	}


	////////////////////////////////////////////////
	//	Contents
	////////////////////////////////////////////////

	public void setPacketContent(byte[] data)
	{
		content = data;
		setContentLength(data.length);
	}

	public void setPacketContent(String data)
	{
    try 
    {
      setPacketContent(data.getBytes("UTF-8"));
    }
    catch( UnsupportedEncodingException e )
    {
    }
	}

	public  byte[] getPacketContent()
	{
		return content;
	}

	public  String getPacketContentString()
	{
    if( content == null )
      return new String("");

    try 
    {
      return new String(content, "UTF-8");
    }
    catch( UnsupportedEncodingException e )
    {
      return new String("");
    }
	}
	
	public boolean hasContent()
	{
		return (content.length > 0) ? true : false;
	}

	public void setContentType(String type)
	{
		setHeader(HTTP.CONTENT_TYPE, type);
	}

	public String getContentType()
	{
		return getHeaderValue(HTTP.CONTENT_TYPE);
	}

	/**
   *  Set the ContentLength header in the packet.  
   *  If len < 0, remove content-length header entirely (packet by
   *  default has a Content-Length = 0, which is not appropriate for
   *  messages with unknown non-zero length).  TODO: sort of a hack
   */
	public void setContentLength(int len)
	{
    if( len < 0 )
    {
      if( removeHeader( HTTP.CONTENT_LENGTH ) == false )
        logger.warning("Error setting content length < 0");
    }
    else
    {
      setIntegerHeader(HTTP.CONTENT_LENGTH, len);
    }
	}

	public int getContentLength()
	{
		return getIntegerHeaderValue(HTTP.CONTENT_LENGTH);
	}

	public void setCacheControl(int len)
	{
		setIntegerHeader(HTTP.CACHE_CONTROL, len);
	}
	public int getCacheControl()
	{
		return getIntegerHeaderValue(HTTP.CACHE_CONTROL);
	}

	/**
   * Set server field - used by server side
   */
	public void setServer(String name)
	{
		setHeader(HTTP.SERVER, name);
	}

	public String getServer()
	{
		return getHeaderValue(HTTP.SERVER);
	}

	public void setHost(String host, int port)
	{
		String hostAddr = host;
		if (HostInterface.isIPv6Address(host) == true)
			hostAddr = "[" + host + "]";
		setHeader(HTTP.HOST, hostAddr + ":" + Integer.toString(port));
	}

	public String getHost()
	{
		return getHeaderValue(HTTP.HOST);
	}

	public void setDate(Calendar cal)
	{
		Date date = new Date(cal);
		setHeader(HTTP.DATE, date.getDateString());
	}

	public String getDate()
	{
		return getHeaderValue(HTTP.DATE);
	}

	////////////////////////////////////////////////
	// set*Value
	////////////////////////////////////////////////

	public void setStringHeader(String name, String value,
                              String startWidth, String endWidth)
	{
		String headerValue = value;
		if (headerValue.startsWith(startWidth) == false)
			headerValue = startWidth + headerValue;
		if (headerValue.endsWith(endWidth) == false)
			headerValue = headerValue + endWidth;
		setHeader(name, headerValue);
	}

	public void setStringHeader(String name, String value)
	{
		setStringHeader(name, value, "\"", "\"");
	}
	
	public String getStringHeaderValue(String name, 
                                     String startWidth, String endWidth)
	{
		String headerValue = getHeaderValue(name);
    if( headerValue == null )
      return null;

		if (headerValue.startsWith(startWidth) == true)
			headerValue = headerValue.substring(1, headerValue.length());
		if (headerValue.endsWith(endWidth) == true)
			headerValue = headerValue.substring(0, headerValue.length()-1);
		return headerValue;
	}
	
	public String getStringHeaderValue(String name)
	{
		return getStringHeaderValue(name, "\"", "\"");
	}

	public void setIntegerHeader(String name, int value)
	{
		setHeader(name, Integer.toString(value));
	}
	
	public int getIntegerHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return 0;
		return StringUtil.toInteger(header.getValue());
	}

	public long getLongHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return 0;
		return StringUtil.toLong(header.getValue());
	}

	/**
   *  Return entire HTTP packet (hdrs + blank line + content) as ASCII string
   */
	public String toString()
	{
		StringBuffer str = new StringBuffer();

		str.append( getHeaderString() );
		str.append( getPacketContentString() );

		return str.toString();
	}


}

